﻿var Hand = require("./hand").Hand
  , Pointable = require("./pointable").Pointable
  , Gesture = require("./gesture").Gesture
  , Vector = require("./vector").Vector
  , Matrix = require("./matrix").Matrix
  , _ = require("underscore");

/**
 * Constructs a Frame object.
 *
 * Frame instances created with this constructor are invalid.
 * Get valid Frame objects by calling the
 * {@link Controller#frame}() function.
 *
 * @class Frame
 * @classdesc
 * The Frame class represents a set of hand and finger tracking data detected
 * in a single frame.
 *
 * The Leap detects hands, fingers and tools within the tracking area, reporting
 * their positions, orientations and motions in frames at the Leap frame rate.
 *
 * Access Frame objects using the {@link Controller#frame}() function.
 */
var Frame = exports.Frame = function(data) {
  /**
   * Reports whether this Frame instance is valid.
   *
   * A valid Frame is one generated by the Controller object that contains
   * tracking data for all detected entities. An invalid Frame contains no
   * actual tracking data, but you can call its functions without risk of a
   * undefined object exception. The invalid Frame mechanism makes it more
   * convenient to track individual data across the frame history. For example,
   * you can invoke:
   *
   * ```javascript
   * var finger = controller.frame(n).finger(fingerID);
   * ```
   *
   * for an arbitrary Frame history value, "n", without first checking whether
   * frame(n) returned a null object. (You should still check that the
   * returned Finger instance is valid.)
   *
   * @member Frame.prototype.valid
   * @type {Boolean}
   */
  this.valid = true;
  /**
   * A unique ID for this Frame. Consecutive frames processed by the Leap
   * have consecutive increasing values.
   * @member Frame.prototype.id
   * @type {String}
   */
  this.id = data.id;
  /**
   * The frame capture time in microseconds elapsed since the Leap started.
   * @member Frame.prototype.timestamp
   * @type {Number}
   */
  this.timestamp = data.timestamp;
  /**
   * The list of Hand objects detected in this frame, given in arbitrary order.
   * The list can be empty if no hands are detected.
   *
   * @member Frame.prototype.hands[]
   * @type {Hand}
   */
  this.hands = [];
  this.handsMap = {};
  /**
   * The list of Pointable objects (fingers and tools) detected in this frame,
   * given in arbitrary order. The list can be empty if no fingers or tools are
   * detected.
   *
   * @member Frame.prototype.pointables[]
   * @type {Pointable}
   */
  this.pointables = [];
  /**
   * The list of Tool objects detected in this frame, given in arbitrary order.
   * The list can be empty if no tools are detected.
   *
   * @member Frame.prototype.tools[]
   * @type {Pointable}
   */
  this.tools = [];
  /**
   * The list of Finger objects detected in this frame, given in arbitrary order.
   * The list can be empty if no fingers are detected.
   * @member Frame.prototype.fingers[]
   * @type {Pointable}
   */

  this.fingers = [];
  this.gestures = [];
  this.pointablesMap = {};
  this._translation = new Vector(data.t);
  this.rotation = new Matrix(data.r);
  this._scaleFactor = data.s;
  this.data = data;
  this.type = 'frame'; // used by event emitting
  var handMap = {};
  for (var handIdx = 0, handCount = data.hands.length; handIdx != handCount; handIdx++) {
    var hand = new Hand(data.hands[handIdx]);
    hand.frame = this;
    this.hands.push(hand);
    this.handsMap[hand.id] = hand;
    handMap[hand.id] = handIdx;
  }
  for (var pointableIdx = 0, pointableCount = data.pointables.length; pointableIdx != pointableCount; pointableIdx++) {
    var pointable = new Pointable(data.pointables[pointableIdx]);
    pointable.frame = this;
    this.pointables.push(pointable);
    this.pointablesMap[pointable.id] = pointable;
    (pointable.tool ? this.tools : this.fingers).push(pointable);
    if (pointable.handId && handMap.hasOwnProperty(pointable.handId)) {
      var hand = this.hands[handMap[pointable.handId]];
      hand.pointables.push(pointable);
      (pointable.tool ? hand.tools : hand.fingers).push(pointable);
    }
  }

  if (data.gestures) {
   /**
    * The list of Gesture objects detected in this frame, given in arbitrary order.
    * The list can be empty if no gestures are detected.
    *
    * Circle and swipe gestures are updated every frame. Tap gestures
    * only appear in the list for a single frame.
    * @member Frame.prototype.gestures[]
    * @type {Gesture}
    */
    for (var gestureIdx = 0, gestureCount = data.gestures.length; gestureIdx != gestureCount; gestureIdx++) {
      this.gestures.push(Gesture(data.gestures[gestureIdx]));
    }
  }
}

/**
 * The tool with the specified ID in this frame.
 *
 * Use the Frame tool() function to retrieve a tool from
 * this frame using an ID value obtained from a previous frame.
 * This function always returns a Pointable object, but if no tool
 * with the specified ID is present, an invalid Pointable object is returned.
 *
 * Note that ID values persist across frames, but only until tracking of a
 * particular object is lost. If tracking of a tool is lost and subsequently
 * regained, the new Pointable object representing that tool may have a
 * different ID than that representing the tool in an earlier frame.
 *
 * @method Frame.prototype.tool
 * @param {String} id The ID value of a Tool object from a previous frame.
 * @returns {Pointable | Pointable.Invalid} The tool with the
 * matching ID if one exists in this frame; otherwise, an invalid Pointable object
 * is returned.
 */
Frame.prototype.tool = function(id) {
  var pointable = this.pointable(id);
  return pointable.tool ? pointable : Pointable.Invalid;
}

/**
 * The Pointable object with the specified ID in this frame.
 *
 * Use the Frame pointable() function to retrieve the Pointable object from
 * this frame using an ID value obtained from a previous frame.
 * This function always returns a Pointable object, but if no finger or tool
 * with the specified ID is present, an invalid Pointable object is returned.
 *
 * Note that ID values persist across frames, but only until tracking of a
 * particular object is lost. If tracking of a finger or tool is lost and subsequently
 * regained, the new Pointable object representing that finger or tool may have
 * a different ID than that representing the finger or tool in an earlier frame.
 *
 * @method Frame.prototype.pointable
 * @param {String} id The ID value of a Pointable object from a previous frame.
 * @returns {Pointable | Pointable.Invalid} The Pointable object with
 * the matching ID if one exists in this frame;
 * otherwise, an invalid Pointable object is returned.
 */
Frame.prototype.pointable = function(id) {
  return this.pointablesMap[id] || Pointable.Invalid;
}

/**
 * The finger with the specified ID in this frame.
 *
 * Use the Frame finger() function to retrieve the finger from
 * this frame using an ID value obtained from a previous frame.
 * This function always returns a Finger object, but if no finger
 * with the specified ID is present, an invalid Pointable object is returned.
 *
 * Note that ID values persist across frames, but only until tracking of a
 * particular object is lost. If tracking of a finger is lost and subsequently
 * regained, the new Pointable object representing that physical finger may have
 * a different ID than that representing the finger in an earlier frame.
 *
 * @method Frame.prototype.finger
 * @param {String} id The ID value of a finger from a previous frame.
 * @returns {Pointable | Pointable.Invalid} The finger with the
 * matching ID if one exists in this frame; otherwise, an invalid Pointable
 * object is returned.
 */
Frame.prototype.finger = function(id) {
  var pointable = this.pointable(id);
  return !pointable.tool ? pointable : Pointable.Invalid;
}

/**
 * The Hand object with the specified ID in this frame.
 *
 * Use the Frame hand() function to retrieve the Hand object from
 * this frame using an ID value obtained from a previous frame.
 * This function always returns a Hand object, but if no hand
 * with the specified ID is present, an invalid Hand object is returned.
 *
 * Note that ID values persist across frames, but only until tracking of a
 * particular object is lost. If tracking of a hand is lost and subsequently
 * regained, the new Hand object representing that physical hand may have
 * a different ID than that representing the physical hand in an earlier frame.
 *
 * @method Frame.prototype.hand
 * @param {String} id The ID value of a Hand object from a previous frame.
 * @returns {Hand | Hand.Invalid} The Hand object with the matching
 * ID if one exists in this frame; otherwise, an invalid Hand object is returned.
 */
Frame.prototype.hand = function(id) {
  return this.handsMap[id] || Hand.Invalid;
}

/**
 * The angle of rotation around the rotation axis derived from the overall 
 * rotational motion between the current frame and the specified frame.
 * 
 * The returned angle is expressed in radians measured clockwise around 
 * the rotation axis (using the right-hand rule) between the start and end frames. 
 * The value is always between 0 and pi radians (0 and 180 degrees).
 * 
 * The Leap derives frame rotation from the relative change in position and 
 * orientation of all objects detected in the field of view.
 * 
 * If either this frame or sinceFrame is an invalid Frame object, then the 
 * angle of rotation is zero.
 * 
 * @method Frame.prototype.rotationAngle
 * @param {Frame} sinceFrame The starting frame for computing the relative rotation.
 * @returns {Number} A positive value containing the heuristically determined 
 * rotational change between the current frame and that specified in the sinceFrame parameter.
 */
Frame.prototype.rotationAngle = function(sinceFrame, axis){
	// TODO: implement axis parameter
	if (!this.valid || !sinceFrame.valid) return 0.0;
	var rot = this.rotationMatrix(sinceFrame);
	var cs = (rot.xBasis.x + rot.yBasis.y + rot.zBasis.z - 1.0)*0.5
	var angle = Math.acos(cs);
	return isNaN(angle) ? 0.0 : angle;
}

/**
 * The axis of rotation derived from the overall rotational motion between 
 * the current frame and the specified frame.
 * 
 * The returned direction vector is normalized.
 * 
 * The Leap derives frame rotation from the relative change in position and 
 * orientation of all objects detected in the field of view.
 * 
 * If either this frame or sinceFrame is an invalid Frame object, or if no 
 * rotation is detected between the two frames, a zero vector is returned.
 * 
 * @method Frame.prototype.rotationAxis
 * @param {Frame} sinceFrame The starting frame for computing the relative rotation.
 * @returns {Vector} A normalized direction Vector representing the axis of the heuristically determined 
 * rotational change between the current frame and that specified in the sinceFrame parameter.
 */
Frame.prototype.rotationAxis = function(sinceFrame){
	if (!this.valid || !sinceFrame.valid) return Vector.zero();
	var x = this.rotation.zBasis.y - sinceFrame.rotation.yBasis.z;
	var y = this.rotation.xBasis.z - sinceFrame.rotation.zBasis.x;
	var z = this.rotation.yBasis.x - sinceFrame.rotation.xBasis.y;
	var vec = new Vector([x, y, z]);
	return vec.normalized();
}

/**
 * The transform matrix expressing the rotation derived from the overall 
 * rotational motion between the current frame and the specified frame.
 * 
 * The Leap derives frame rotation from the relative change in position and 
 * orientation of all objects detected in the field of view.
 * 
 * If either this frame or sinceFrame is an invalid Frame object, then 
 * this method returns an identity matrix.
 * 
 * @method Frame.prototype.rotationMatrix
 * @param {Frame} sinceFrame The starting frame for computing the relative rotation.
 * @returns {Matrix} A transformation Matrix containing the heuristically determined 
 * rotational change between the current frame and that specified in the sinceFrame parameter.
 */
Frame.prototype.rotationMatrix = function(sinceFrame){
	if (!this.valid || !sinceFrame.valid) return Matrix.identity();
	var xBasis = new Vector([this.rotation.xBasis.x, this.rotation.yBasis.x, this.rotation.zBasis.x]);
	var yBasis = new Vector([this.rotation.xBasis.y, this.rotation.yBasis.y, this.rotation.zBasis.y]);
	var zBasis = new Vector([this.rotation.xBasis.z, this.rotation.yBasis.z, this.rotation.zBasis.z]);
	var transpose = new Matrix([xBasis, yBasis, zBasis]);
	return sinceFrame.rotation.times(transpose);
}

/**
 * The scale factor derived from the overall motion between the current frame and the specified frame.
 * 
 * The scale factor is always positive. A value of 1.0 indicates no scaling took place. 
 * Values between 0.0 and 1.0 indicate contraction and values greater than 1.0 indicate expansion.
 * 
 * The Leap derives scaling from the relative inward or outward motion of all 
 * objects detected in the field of view (independent of translation and rotation).
 * 
 * If either this frame or sinceFrame is an invalid Frame object, then this method returns 1.0.
 * 
 * @method Frame.prototype.scaleFactor
 * @param {Frame} sinceFrame The starting frame for computing the relative scaling.
 * @returns {Number} A positive value representing the heuristically determined 
 * scaling change ratio between the current frame and that specified in the sinceFrame parameter.
 */
Frame.prototype.scaleFactor = function(sinceFrame){
	if (!this.valid || !sinceFrame.valid) return 1.0;
	return Math.exp(this._scaleFactor - sinceFrame._scaleFactor);
}

/**
 * The change of position derived from the overall linear motion between the 
 * current frame and the specified frame.
 * 
 * The returned translation vector provides the magnitude and direction of the 
 * movement in millimeters.
 * 
 * The Leap derives frame translation from the linear motion of all objects 
 * detected in the field of view.
 * 
 * If either this frame or sinceFrame is an invalid Frame object, then this 
 * method returns a zero vector.
 * 
 * @method Frame.prototype.translation
 * @param {Frame} sinceFrame The starting frame for computing the relative translation.
 * @returns {Vector} A Vector representing the heuristically determined change in 
 * position of all objects between the current frame and that specified in the sinceFrame parameter.
 */
Frame.prototype.translation = function(sinceFrame){
	if (!this.valid || !sinceFrame.valid) return Vector.zero();
	var x = this._translation.x - sinceFrame._translation.x;
	var y = this._translation.y - sinceFrame._translation.y;
	var z = this._translation.z - sinceFrame._translation.z;
	return new Vector([x, y, z]);
}

/**
 * A string containing a brief, human readable description of the Frame object.
 *
 * @method Frame.prototype.toString
 * @returns {String} A brief description of this frame.
 */
Frame.prototype.toString = function() {
  var str = "Frame [ id:"+this.id+" | timestamp:"+this.timestamp+" | Hand count:("+this.hands.length+") | Pointable count:("+this.pointables.length+")";
  if (this.gestures) str += " | Gesture count:("+this.gestures.length+")";
  str += " ]";
  return str;
}

/**
 * Returns a JSON-formatted string containing the hands, pointables and gestures
 * in this frame.
 *
 * @method Frame.prototype.dump
 * @returns {String} A JSON-formatted string.
 */
Frame.prototype.dump = function() {
  var out = '';
  out += "Frame Info:<br/>";
  out += this.toString();
  out += "<br/><br/>Hands:<br/>"
  for (var handIdx = 0, handCount = this.hands.length; handIdx != handCount; handIdx++) {
    out += "  "+ this.hands[handIdx].toString() + "<br/>";
  }
  out += "<br/><br/>Pointables:<br/>";
  for (var pointableIdx = 0, pointableCount = this.pointables.length; pointableIdx != pointableCount; pointableIdx++) {
      out += "  "+ this.pointables[pointableIdx].toString() + "<br/>";
  }
  if (this.gestures) {
    out += "<br/><br/>Gestures:<br/>";
    for (var gestureIdx = 0, gestureCount = this.gestures.length; gestureIdx != gestureCount; gestureIdx++) {
        out += "  "+ this.gestures[gestureIdx].toString() + "<br/>";
    }
  }
  out += "<br/><br/>Raw JSON:<br/>";
  out += JSON.stringify(this.data);
  return out;
}

/**
 * An invalid Frame object.
 *
 * You can use this invalid Frame in comparisons testing
 * whether a given Frame instance is valid or invalid. (You can also check the
 * {@link Frame#valid} property.)
 *
 * @constant
 * @type {Frame}
 * @name Frame.Invalid
 */
Frame.Invalid = {
  valid: false,
  hands: [],
  fingers: [],
  tools: [],
  gestures: [],
  pointables: [],
  pointable: function() { return Pointable.Invalid },
  finger: function() { return Pointable.Invalid },
  hand: function() { return Hand.Invalid },
  toString: function() { return "invalid frame" },
  dump: function() { return this.toString() }
}
